Skip to content

Require authentication for GraphQL WebSocket upgrade (unomi-3.0.x) - #844

Open
sergehuber wants to merge 3 commits into
unomi-3.0.xfrom
backport/graphql-websocket-auth-3.0.x
Open

Require authentication for GraphQL WebSocket upgrade (unomi-3.0.x)#844
sergehuber wants to merge 3 commits into
unomi-3.0.xfrom
backport/graphql-websocket-auth-3.0.x

Conversation

@sergehuber

@sergehuber sergehuber commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Backport of Require authentication for GraphQL WebSocket upgrade (master) #843 to unomi-3.0.x, re-cut from the current branch head.
  • A handshake that carries an Authorization header is authenticated before the upgrade is accepted, and a handshake that declares a foreign origin is refused.
  • A handshake that carries no credential — which is all a browser can send — is upgraded, but the resulting socket does nothing until it authenticates through the connection_init payload: any other message closes it, and a socket that has not authenticated within a short deadline is closed.
  • The shipped GraphQL UI passes the Headers-tab Authorization to the WebSocket client as connectionParams, so both transports use the same credential.
  • Differences from master, because this line has no tenancy or security context: whether a socket is authenticated is a flag set from the remote user recorded on a successful handshake login, and the deadline scheduler is stopped from the servlet's destroy(). Close frames now carry valid close codes.

Test plan

  • GraphQLWebSocketIT extended to 8 cases and GraphQLServletSecurityIT registered in AllITs; both pass locally under Karaf + Elasticsearch 9.1.3
  • CI green on b9b38b132 (unit + integration)

🤖 Generated with Claude Code

@sergehuber sergehuber changed the title Require authentication for GraphQL WebSocket upgrade Require authentication for GraphQL WebSocket upgrade (unomi-3.0.x) Aug 7, 2026
Port of the master fix (#843) to the 3.0.x line, re-cut from current unomi-3.0.x.

A handshake that carries an Authorization header is authenticated before the
upgrade is accepted, and a foreign-origin handshake is refused, since a
WebSocket handshake is not subject to the same-origin policy. A handshake that
carries no credential - which is all a browser can send - is upgraded but the
resulting socket does nothing until it authenticates through the connection_init
payload; every other message is refused and closes the socket, and a scheduled
close ends any socket that has not authenticated within its deadline. The
shipped GraphQL UI passes the Headers-tab Authorization to the WebSocket client
as connectionParams so both transports use the same credential.

Differences from master, because 3.0.x has no tenancy or security context:
whether a socket is authenticated is a plain flag set from the remote user the
validator records on a successful handshake login, and there is no execution
context to bind around event delivery. The deadline scheduler is shut down from
the servlet's destroy(), as the creator object has no Jetty lifecycle of its
own. Close frames now carry valid codes (1000/1008) instead of 0, and the
credential payload is not logged.

Covered by integration tests only, as this line does not carry a unit-test
stack for the GraphQL module. GraphQLServletSecurityIT is now registered in
AllITs; it was never run on this line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@asf-gitbox-commits
asf-gitbox-commits force-pushed the backport/graphql-websocket-auth-3.0.x branch from b9f6d6f to 6d55e73 Compare September 4, 2026 10:13
JUnit resolves the types in a test class's method signatures when it scans the
class, which happens at probe start, before the test's setup has waited for the
container to finish provisioning. A helper that took a websocket-client type as
a parameter therefore made every test in the class fail to load whenever the
class ran early, as it does when selected on its own. Build the upgrade request
inside the helper instead, so the client bundle is only needed once the tests
actually run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Browser-authenticated sockets disable normal idle limits, and the new Basic authentication path has standards-compliance issues.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Backports authenticated GraphQL WebSocket handling to the 3.0.x branch.

Changes:

  • Authenticates header-bearing upgrades and rejects foreign origins.
  • Requires browser sockets to authenticate through connection_init.
  • Forwards GraphiQL credentials and expands integration coverage.
File summaries
File Description
init-with-credentials.json Adds valid initialization fixture.
init-bad-credentials.json Adds invalid initialization fixture.
GraphQLWebSocketIT.java Tests authentication and origin rejection.
AllITs.java Registers security integration tests.
index.jsx Forwards authorization through WebSocket parameters.
SubscriptionWebSocketFactory.java Creates gated sockets and manages deadlines.
SubscriptionWebSocket.java Enforces authentication before operations.
GraphQLServlet.java Secures WebSocket upgrades.
GraphQLServletSecurityValidator.java Adds WebSocket Basic authentication.
Review details
  • Files reviewed: 7/9 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

if (isAuthenticatedUser(req)) {
return true;
}
res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
* @param req the originating request, or {@code null} when the credential did not arrive on one
*/
private boolean authenticateBasic(String authHeader, HttpServletRequest req) {
if (authHeader == null || !authHeader.startsWith("Basic ")) {
final Session session = getSession();
if (session != null) {
// Authenticated: drop the short unauthenticated deadline.
session.setIdleTimeout(0);
…ore idle timeout

A WebSocket upgrade whose credential is refused now receives the same
WWW-Authenticate challenge as one that carries no credential, as a 401 must.
The Basic scheme token is compared case-insensitively, as HTTP authentication
schemes are. A socket that authenticates through connection_init now goes back
to the idle timeout its session was configured with instead of having the
timeout disabled, so both authentication paths share the same idle behaviour.

Adds an integration test authenticating with a lowercase scheme on the
handshake.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants